上一章我們學到 Button
,按鈕都是在需要改變狀態的時候使用的,例如,增/減數量、輸入數值、登入狀態。
要做到這一點,最基本的是需要能改變 View
內的變數。
我們先在 ContentView
內寫上以下程式碼,預期每按一下按鈕,count 就會加一:
struct ContentView: View {
private var count: Int = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Add Count") {
count += 1
}
.buttonStyle(.borderedProminent)
}
}
}
不過這時會發現,這邊的 count 不可變,會噴錯。
這時候就要輪到 State
登場了!
@State
是一個 屬性包裝器
(Property Wrapper
),他會將變數做處理並監聽他的 記憶體內容
變化,當變數 記憶體內容
改變時,@State
就會通知 View
要更新顯示內容。
我們將 count 變數前面加上 @State
,接著點擊看看按鈕,count 會有什麼變化?
@State private var count: Int = 0
count 加上
@State
後可以被按鈕改變。每次改變,畫面顯示的數字也會跟著改變。
那麼現在 count 可以改變了,那假設我們想把 count 傳入另一個 View
,讓他也可以被改變呢?
我們試試看在 AnotherView
內這樣寫:
struct AnotherView: View {
@State var count: Int
var body: some View {
VStack {
Text("AnotherView")
.font(.title)
Text("Count: \(count)")
Button("Add Count") {
count += 1
}
.buttonStyle(.borderedProminent)
}
.padding()
.border(Color.red)
}
}
struct AnotherView_Previews: PreviewProvider {
static var previews: some View {
AnotherView(count: 0)
}
然後在 ContentView
裡加上 AnotherView
:
struct ContentView: View {
@State private var count: Int = 0
var body: some View {
VStack {
Text("ContentView")
.font(.title)
Text("Count: \(count)")
Button("Add Count") {
count += 1
}
.buttonStyle(.borderedProminent)
AnotherView(count: count)
}
}
}
這時按一下 ContentView
和 AnotherView
中的按鈕會發現:
AnotherView
裡的 count 增加並不會使 ContentView
的 count 也增加。
這兩個 View
的 count 不是同一個!
如果我們要使兩個 View
的 count 都是同 記憶體
的話,需要使用 Binding
。
@Binding
是一個 屬性包裝器
(Property Wrapper
)。它的功用跟 @State
很像,只是它是需要綁定 @State
來源。什麼意思呢?我們看實際例子。
我們把 AnotherView
的 count @State
改成 @Binding
:
struct AnotherView: View {
@Binding var count: Int
...
}
struct AnotherView_Previews: PreviewProvider {
static var previews: some View {
AnotherView(count: .constant(0))
}
}
注意
AnotherView_Previews
當傳入的參數需要是Binding
時,可以用.constant()
來傳入參數,但這樣畫面並不會跟著變動。可以另外寫個
AnotherPreview
的View
來包裝AnotherView
:
struct AnotherView_Previews: PreviewProvider {
static var previews: some View {
AnotherPreview(count: 0)
}
}
struct AnotherPreview: View {
@State var count: Int
var body: some View {
AnotherView(count: $count)
// Binding 傳入的 State 參數,前面要加上 $
}
}
ContentView
中的 AnotherView
,在傳入的 count 加上一個 $
,表示我們傳入的是 Binding
AnotherView(count: $count)
這時候再點兩個按鈕看看,你會發現任何一個 View
的按鈕都可以同時影響到兩個 count!
當我們的變數,不是一個單純的數字或結構而是 class
時,你會發現按鈕又無效了!例如下面這個例子:
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
struct ContentView: View {
@State private var person = Person(name: "Faker", age: 5)
var body: some View {
VStack {
Text("ContentView")
.font(.title)
Text("Count: \(person.age)")
Button("Add Age") {
person.age += 1
}
.buttonStyle(.borderedProminent)
}
}
這是因為 @State
關注的是 記憶體內容
的變化,而 class
裡面數值的變化,並不會影響 class
本身 記憶體內容
的變化(class
的 記憶體內容
是一個 位址
)
這時就需要用到 @StateObject
屬性包裝器。
@StateObject
和 @State
有一樣的功用,只是它是給結構使用的,他會監聽結構內的變化。
我們把 ContentView
的 person 前面加上 @StateObject
:
@StateObject private var person = Person(name: "Faker", age: 5)
這時候他會跳出錯誤,告訴我們使用 @StateObject
的物件需要實作 ObservableObject
這個 Protocol
:
我們把 Person
這個 class
實作 ObservableObject
,然後在想要監聽的屬性前加上 @Published
:
class Person: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
@Published
的功用是,該屬性如果改變,回通知最外層的@StateObject
讓他刷新View
此時我們的按鈕更改 person.age 就可以更新在顯示畫面上了。
那如果是其他的 View
要傳入 StateObject
的物件呢?
這時候就是 @ObservedObject
屬性包裝器上場的時候了。
如果要很簡單的理解 @ObservedObject
的話,可以這樣想:
@State
之於@Binding
就類似@StateObject
之於@ObservedObject
所以如果我們要在 AnotherView
裡傳入 person 的話,可以這樣寫:
struct AnotherView: View {
@ObservedObject var person: Person
var body: some View {
VStack {
Text("AnotherView")
.font(.title)
Text("Age: \(person.age)")
Button("Add Age") {
person.age += 1
}
.buttonStyle(.borderedProminent)
}
.padding()
.border(Color.red)
}
}
struct AnotherView_Previews: PreviewProvider {
static var previews: some View {
AnotherPreview(person: Person(name: "Faker", age: 5))
}
}
struct AnotherPreview: View {
@State var person: Person
var body: some View {
AnotherView(person: person)
}
}
最後 ContentView
加上 AnotherView
就大功告成啦!
struct ContentView: View {
@StateObject private var person = Person(name: "Faker", age: 5)
var body: some View {
VStack {
Text("ContentView")
.font(.title)
Text("Age: \(person.age)")
Button("Add Age") {
person.age += 1
}
.buttonStyle(.borderedProminent)
AnotherView(person: person)
}
}
}
要改變變數數值,並可以根據變數改動來刷新畫面,需要使用 @State
, @Binding
, @StateObject
, @ObservedObject
@State
, @Binding
只會關注 記憶體內容
的變化
@StateObject
, @ObservedObject
會關注 ObservableObject
內有添加 @Published
的屬性變化
對不同物件和用途,要選擇不同的屬性包裝器,可參考下表:
物件類型 | 舉例 | 初始 / 新建的變數 | 傳遞變數 |
---|---|---|---|
傳值物件 Pass by Value |
Int, Struct | @State |
@Binding |
傳址物件 Pass by Address |
Class | @StateObject |
@ObservedObject |
@StateObject
和 @ObservedObject
的關係其實有一點複雜,想深入研究可以參考以下文章: